package com.smartalarmclock.Model;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

import com.smartalarmclock.Model.Exceptions.DataEntityDoesNotExistException;
import com.smartalarmclock.Model.Exceptions.InvalidDeleteDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.InvalidNullReferenceOnDeleteException;
import com.smartalarmclock.Model.Exceptions.InvalidReferenceToRemoveException;
import com.smartalarmclock.Model.Exceptions.InvalidSaveDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.NamedGroupAlreadyExistsException;
import com.smartalarmclock.Model.Reference.DeleteMethod;


import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

final class Group extends Contact implements IGroup, IReferencingMultipleDataEntity {
	
	// Database constants
	
	public static final String GroupToContactsTableName = "GroupToContacts";
	
	private static final String c_GroupToContactsColumnNameGroupId = "GroupId";
	private static final String c_GroupToContactsColumnNameContactId = "ContactId";
	
	private static final String[] c_QueryAllColumnsContact = new String[] { c_ColumnNameId, c_ContactColumnNameName };
	private static final String[] c_QueryAllColumnsGroupToContacts = new String[] { c_GroupToContactsColumnNameGroupId, c_GroupToContactsColumnNameContactId };
	
	// Members
	
	private String m_Name;
	private final MultipleReference<IContact> m_ContactReferences;
	
	// Constructors
	
	/**
	 * Public Constructor for a new Group
	 * @param name the name of the group
	 */
	protected Group(String name) {
		super();
		m_Name = name;
		m_ContactReferences = new MultipleReference<IContact>(this, Contact.class, DeleteMethod.NoAction);
	}

	/**
	 * Private Constructor for creation after querying an existing group
	 * @param id the id in the DB of the matching group
	 * @param name the name of the group
	 */
	private Group(int id, String name) {
		super(id);
		m_Name = name;
		m_ContactReferences = new MultipleReference<IContact>(this, Contact.class, DeleteMethod.NoAction);
	}
	
	// IGroup implementation
	
	/**
	 * @return the group name
	 */
	public String GetName() {
		return m_Name;
	}
	
	/**
	 * Sets the group's name
	 * @param name the new group name
	 */
	public void SetName(String name) {
		m_Name = name;
		SetUpdated();
	}
	
	/**
	 * @param context the context of the request (the originating Activity)
	 * @return a set of the group's contact list
	 */
	public Set<IContact> GetContacts(Context context) {
		return m_ContactReferences.GetReferences(context);
	}
	
	/**
	 * Adds a contact to the group
	 * @param context the context of the request (the originating Activity)
	 * @param contact the contact to add
	 */
	public void AddContact(Context context, IContact contact) {
		m_ContactReferences.Add(context, contact);
		SetUpdated();
	}
	
	/**
	 * Removes a contact from the group
	 * @param context the context of the request (the originating Activity)
	 * @param contact the contact to remove
	 */
	public void RemoveContact(Context context, IContact contact) {
		m_ContactReferences.Remove(context, contact);
		SetUpdated();
	}
	
	// Public Methods
	
	/**
	 * @return the CREATE TABLE statement of the GroupToContacts table
	 */
	static String GetSqlStatementCreateGroupToContactsTable() {
		return "CREATE TABLE " + GroupToContactsTableName + " (" +
				c_GroupToContactsColumnNameGroupId + 	" INTEGER NOT NULL, " +
				c_GroupToContactsColumnNameContactId + 	" INTEGER NOT NULL, " +
				"PRIMARY KEY (" + c_GroupToContactsColumnNameGroupId + ", " + c_GroupToContactsColumnNameContactId + ")," +
				"FOREIGN KEY(" + c_GroupToContactsColumnNameGroupId   + ") REFERENCES " + Contact.ContactTableName + "(" + c_ColumnNameId + ")," +
				"FOREIGN KEY(" + c_GroupToContactsColumnNameContactId + ") REFERENCES " + Contact.ContactTableName + "(" + c_ColumnNameId + "))";
	}
	
	/**
	 * @return the DROP TABLE statement of the GroupToCotacts table
	 */
	static String GetSqlStatementDropGroupToContactsTable() {
		return "DROP TABLE IF EXISTS " + GroupToContactsTableName;
	}
	
	/**
	 * @param context the context of the request (the originating Activity)
	 * @param id the id of the group
	 * @return the Group instance from the DB with the corresponding id
	 * @throws DataEntityDoesNotExistException if the group was not found
	 */
	static Group GetGroupById(Context context, int id) throws DataEntityDoesNotExistException {
		return QueryGroup(
			context,
			c_ContactColumnNameType + "=" + ContactType.Group.getValue() + " AND " +
			c_ColumnNameId + "=" + id);
	}
	
	/**
	 * @param context the context of the request (the originating Activity)
	 * @param id the id of the group
	 * @return the Group instance from the DB with the corresponding id, null if doesn't exist
	 */
	static Group TryGetGroupById(Context context, int id) {
		try {
			return GetGroupById(context, id);
		} catch (DataEntityDoesNotExistException e) {
			return null;
		}
	}
	
	/**
	 * @param context the context of the request (the originating Activity)
	 * @param name the name of the group
	 * @return the Group instance from the DB with the corresponding name
	 * @throws DataEntityDoesNotExistException if the group was not found
	 */
	static Group GetGroupByName(Context context, String name) throws DataEntityDoesNotExistException {
		return QueryGroup(
			context,
			c_ContactColumnNameType + "=" + ContactType.Group.getValue() + " AND " +
			c_ContactColumnNameName + " LIKE \"" + name + "\"");
	}
	
	/**
	 * @param context the context of the request (the originating Activity)
	 * @param name the name of the group
	 * @return the Group instance from the DB with the corresponding name, null if doesn't exist
	 */
	static Group TryGetGroupByName(Context context, String name) {
		try {
			return GetGroupByName(context, name);
		} catch (DataEntityDoesNotExistException e) {
			return null;
		}
	}
	
	/**
	 * @param context the context of the request (the originating Activity)
	 * @return all the contact groups
	 */
	static Set<IGroup> GetAllGroups(Context context) {
		SQLiteDatabase db = OpenDBForWriting(context);
		Cursor cursor = db.query(
			ContactTableName,
			c_QueryAllColumnsContact,
			c_ContactColumnNameType + "=" + ContactType.Group.getValue(),
			null, null, null, null);
		
		Set<IGroup> groups = new HashSet<IGroup>();
		
		while (cursor.moveToNext()) {
			groups.add(CreateGroupFromCursor(cursor));
		}
		
		cursor.close();
		db.close();
		return groups;
	}
	
	/**
	 * @param context the context of the request (the originating Activity)
	 * @return all groups containing the given contact.
	 */
	static Set<IGroup> GetGroupsByContact(Context context, IContact contact) {
		SQLiteDatabase db = OpenDBForWriting(context);
		Cursor cursor = db.query(
			GroupToContactsTableName,
			c_QueryAllColumnsGroupToContacts,
			c_GroupToContactsColumnNameContactId + "=" + contact.GetId(),
			null, null, null, null);
		
		Set<IGroup> groups = new HashSet<IGroup>();
		
		int groupId = 0;
		while (cursor.moveToNext()) {
			try {
				groupId = cursor.getInt(cursor.getColumnIndex(c_GroupToContactsColumnNameGroupId));
				groups.add(GetGroupById(context, groupId));
			} catch (DataEntityDoesNotExistException e) {
				// This cannot happen since we retrieved the id from the DB. 
			}
		}
		
		cursor.close();
		db.close();
		return groups;
	}
	
	// Private Methods
		
	/**
	 * Creates a new Group instance from the values of the record in the cursor
	 * @param cursor the DB query cursor placed on a Group valid record.
	 * @return the corresponding Group instance
	 */
	private static Group CreateGroupFromCursor(Cursor cursor) {
		int id = cursor.getInt(cursor.getColumnIndex(c_ColumnNameId));
		String name = cursor.getString(cursor.getColumnIndex(c_ContactColumnNameName));
		return new Group(id, name);
	}
	
	/**
	 * Queries the DB for a single record with the corresponding where clause 
	 * @param context the context of the request (the originating Activity)
	 * @param whereClause the where clause for the query
	 * @return the Group instance with the matching record's values.
	 * @throws DataEntityDoesNotExistException if the Group is not found in the database.
	 */
	private static Group QueryGroup(Context context, String whereClause) throws DataEntityDoesNotExistException {
		SQLiteDatabase db = OpenDBForReading(context);
		Cursor cursor = db.query(ContactTableName, c_QueryAllColumnsContact, whereClause, null, null, null, null);
		
		try {
			if (!cursor.moveToNext()) {
				throw new DataEntityDoesNotExistException(SingleContact.class, whereClause);
			}
			
			Group newGroup = CreateGroupFromCursor(cursor);
			return newGroup;
		}
		finally {
			cursor.close();
			db.close();
		}
	}
	
	// DataEntity implementation
	
	/**
	 * @return the ContentValues containing the values of the object for the DB
	 */
	@Override
	protected Hashtable<String, ContentValues> GetRecordValues() {
		ContentValues values = new ContentValues();
		
		values.put(c_ContactColumnNameType, ContactType.Group.getValue());
		values.put(c_ContactColumnNameName, m_Name);
		
		Hashtable<String, ContentValues> valuesTable = new Hashtable<String, ContentValues>();
		valuesTable.put(ContactTableName, values);
		return valuesTable;
	}
	
	// IDataEntity implementation
	
	/**
	 * Loads all members again from the DB and updates the object in memory with the current values.
	 * The loading process uses the object's id.
	 * @param context the context of the request
	 * @throws DataEntityDoesNotExistException if the entity with the current id does not exist in the DB,
	 * or if the object was not yet saved in the DB.
	 */
	@Override
	public void Refresh(Context context) throws DataEntityDoesNotExistException {
		Group tempGroup = GetGroupById(context, GetId());
		m_Name = tempGroup.GetName();
		m_ContactReferences.Refresh(context);
	}
	
	/**
	 * Saves the instance in the database and all contact connections in the group.
	 * If contacts were removed from the group, their connections are deleted and the matching
	 * instances were removed from the groups contacts list (in memory).
	 * @throws NamedGroupAlreadyExistsException if a group with the same name appears in the DB.
	 */
	@Override
	public void Save(Context context) throws InvalidSaveDataEntityOperationException {
		if (IsNewInstance() && TryGetGroupByName(context, m_Name) != null) {
			// A group with the same name already exists
			throw new NamedGroupAlreadyExistsException(m_Name);
		}
		
		if (IsUpdated()) {
			// Save the group as a contact
			super.Save(context);
			
			// Save all contacts in the group
			m_ContactReferences.SaveReferences(context);
		}
	}
	
	/**
	 * Deletes the instance from the database and all contact connections.
	 * The contacts themselves are not deleted from the DB.
	 */
	@Override
	public void Delete(Context context) throws InvalidDeleteDataEntityOperationException, InvalidSaveDataEntityOperationException {
		// Removing all contacts from the group
		m_ContactReferences.OnDeleteReferencingEntity(context);
		
		// Deleting the group itself.
		super.Delete(context);
	}

	// IRefrencingDataEntity implementation

	/**
	 * Checks if the given entity is actually referenced by the the referencing entity
	 * @param context the context of the request
	 * @param referencedEntity the referenced entity.
	 * @return true if the entity is referenced, otherwise false.
	 */
	@Override
	public boolean IsReferenced(Context context, IReferencedDataEntity referencedEntity) {
		return m_ContactReferences.GetReferences(context).contains(referencedEntity);
	}

	/**
	 * Checks if the removal of the reference is valid. If not this means that the reference cannot be null.
	 * @param context the context of the request
	 * @param referencedEntity the referenced entity.
	 * @return true if the entity can be removed safely, otherwise false.
	 */
	@Override
	public boolean CanRemoveReference(Context context, IReferencedDataEntity referencedEntity) {
		// All referenced contacts can be removed.
		return IsReferenced(context, referencedEntity);
	}

	/**
	 * Removes the reference of the referenced data entity.
	 * @param context the context of the request
	 * @param referencedEntity the referenced entity.
	 * @throws InvalidNullReferenceOnDeleteException if the reference cannot be null and the removal is invalid.
	 * @throws InvalidReferenceToRemoveException if the referenced entity is not referenced at all.
	 */
	@Override
	public void RemoveReference(Context context, IReferencedDataEntity referencedEntity) throws InvalidNullReferenceOnDeleteException, InvalidReferenceToRemoveException {
		if (!IsReferenced(context, referencedEntity)) {
			throw new InvalidReferenceToRemoveException(getClass(), GetId(), referencedEntity.getClass(), referencedEntity.GetId());
		}
		
		RemoveContact(context, (IContact)referencedEntity);
	}
	
	// IReferencingMultipleDataEntity

	/**
	 * Saves the references in the referencing table in the DB.
	 * @param context the context of the request 
	 * @param referencedEntity the referenced entity to save
	 */
	@Override
	public void SaveReferenceConnection(Context context, IReferencedDataEntity referencedEntity) {
		SQLiteDatabase db = OpenDBForWriting(context);
		
		// Checking if the connection exists
		Cursor cursor = db.query(
			GroupToContactsTableName,
			c_QueryAllColumnsGroupToContacts,
			c_GroupToContactsColumnNameGroupId + "=" + GetId() + " AND " + c_GroupToContactsColumnNameContactId + "=" + referencedEntity.GetId(),
			null, null, null, null);
		
		if (!cursor.moveToNext()) {
			// If no records were queried, add the connection between the contact and the group
			ContentValues values = new ContentValues();
			values.put(c_GroupToContactsColumnNameGroupId, GetId());
			values.put(c_GroupToContactsColumnNameContactId, referencedEntity.GetId());
			
			db.insert(GroupToContactsTableName, null, values);
		}
		
		cursor.close();
		db.close();
	}

	/**
	 * Deletes the references from the referencing table in the DB.
	 * @param context the context of the request
	 * @param referencedEntity the referenced entity to delete
	 */
	@Override
	public void DeleteReferenceConnection(Context context, IReferencedDataEntity referencedEntity) {
		SQLiteDatabase db = OpenDBForWriting(context);
		
		// Checking if the connection exists
		Cursor cursor = db.query(
			GroupToContactsTableName,
			c_QueryAllColumnsGroupToContacts,
			c_GroupToContactsColumnNameGroupId + "=" + GetId() + " AND " + c_GroupToContactsColumnNameContactId + "=" + referencedEntity.GetId(),
			null, null, null, null);
		
		if (cursor.moveToNext()) {
			db.delete(
				GroupToContactsTableName,
				c_GroupToContactsColumnNameGroupId + "=" + GetId() + " AND " + c_GroupToContactsColumnNameContactId + "=" + referencedEntity.GetId(),
				null);
		}
		
		cursor.close();
		db.close();
	}

	/**
	 * Loads the references from the referencing table in the DB.
	 * @param context the context of the request
	 * @param referencedType the type of the references to load.
	 */
	@Override
	public void LoadReferences(Context context, Class<?> referencedType) {
		if (Contact.class == referencedType) {
			SQLiteDatabase db = OpenDBForReading(context);
			Cursor cursor = db.query(
				GroupToContactsTableName,
				c_QueryAllColumnsGroupToContacts,
				c_GroupToContactsColumnNameGroupId + "=" + GetId(),
				null, null, null, null);
			
			int contactId = 0;
			IContact contact = null;
			ContactFactory factory = new ContactFactory();
			
			while (cursor.moveToNext()) {
				contactId = cursor.getInt(cursor.getColumnIndex(c_GroupToContactsColumnNameContactId));
				contact = factory.TryGetContactById(context, contactId);
				m_ContactReferences.Add(context, contact);
			}
			
			cursor.close();
			db.close();
		}
	}
	
	// IContact implementation

	/**
	 * @return whether the contact is a single contact or not.
	 */
	@Override
	public boolean IsSingleContact() {
		return false;
	}

	/**
	 * @return whether the contact is a group or not.
	 */
	@Override
	public boolean IsGroup() {
		return true;
	}
	
	@Override
	public String GetName(Context context) {
		return GetName();
	}
	
	// Object implementation
	
	@Override
	public String toString() {
		return String.format("%s group", GetName());
	}
	
	@Override
	public int hashCode() {
		return toString().hashCode();
	}
}
